Frédéric Yhuel
Cette présentation est distribuée sous la licence CC BY-SA 4.0.
Frédéric Yhuel, consultant DBA depuis 4 ans chez Dalibo.
Support, audits et formations.
Scripts pour reproduire :
Nested Loop et arrondi
QUERY PLAN
------------------------------------------------------------------------
Limit (actual time=13.623..13.624 rows=10 loops=1)
-> Sort (actual time=13.620..13.622 rows=10 loops=1)
Sort Key: o.quantity DESC
Sort Method: top-N heapsort Memory: 26kB
-> Nested Loop (actual time=0.201..13.575 rows=117 loops=1)
-> Seq Scan on products p
(actual time=0.077..12.607 rows=381 loops=1)
Filter: ((name ~~ 'ed%'::text) AND
(short_name ~~ 'ed%'::text))
Rows Removed by Filter: 99619
-> Index Scan using orders_product_id_idx on orders o
(actual time=0.002..0.002 rows=0 loops=381)
Index Cond: (product_id = p.id)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8570b14f62..e8f603b145 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1661,11 +1661,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
{
if (es->timing)
appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
+ " (actual time=%.3f..%.3f rows=%.1f loops=%.0f)",
startup_ms, total_ms, rows, nloops);
else
appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
+ " (actual rows=%.1f loops=%.0f)",
rows, nloops);
QUERY PLAN
------------------------------------------------------------------------
Limit (actual time=13.623..13.624 rows=10.0 loops=1)
-> Sort (actual time=13.620..13.622 rows=10.0 loops=1)
Sort Key: o.quantity DESC
Sort Method: top-N heapsort Memory: 26kB
-> Nested Loop (actual time=0.201..13.575 rows=117.0 loops=1)
-> Seq Scan on products p
(actual time=0.077..12.607 rows=381.0 loops=1)
Filter: ((name ~~ 'ed%'::text) AND
(short_name ~~ 'ed%'::text))
Rows Removed by Filter: 99619
-> Index Scan using orders_product_id_idx on orders o
(actual time=0.002..0.002 rows=0.3 loops=381)
Index Cond: (product_id = p.id)
Et donc pas de vrai problème ici, juste un soucis d’affichage qui peut être déroutant, même pour un pratiquant aguerri des plans d’exécution.
Jointure et parallélisation
INSERT INTO products (name, price)
SELECT md5(i::TEXT), random() * 1000 FROM generate_series(1, 400000) i;
INSERT INTO orders (product_id, quantity)
SELECT (i%1000) + 1, random()*10 FROM generate_series(1, 3000000) i;
INSERT INTO orders (product_id, quantity)
SELECT (i%400000) + 1, random()*10 FROM generate_series(1, 3000000) i;
On a donc une répartition très hétérogène dans la table orders, avec la moitié des lignes qui référencent seulement de la table products. Malgré un ANALYZE effectué après les insertions, les statistiques seront faussées.
Pour une catégorie de prix donné, on veut le top 10 des produits qui se vendent le mieux.
Autrement dit, PostgreSQL estime que le processus leader traite
et plus rien au-delà.
Parfois très différent de la réalité !
EXPLAIN (ANALYZE, VERBOSE) …
-> Parallel Seq Scan on public.products p
(cost=0.00..5822.33 rows=162 width=41)
(actual time=0.077..14.569 rows=130 loops=3)
Output: p.id, p.name, p.price
Filter: (p.price = 769)
Rows Removed by Filter: 133204
Worker 0: actual time=0.005..11.914 rows=126 loops=1
Worker 1: actual time=0.019..11.559 rows=80 loops=1
nb_workers = 2
-> Nested Loop (cost=0.43..19818.47 rows=2431 width=37)
(actual time=0.071..55.054 rows=3969 loops=3)
-> Parallel Seq Scan on products p
(cost=0.00..5822.33 rows=162 width=41)
(actual time=0.025..12.600 rows=130 loops=3)
Filter: (price = 769)
Rows Removed by Filter: 133204
-> Index Scan using orders_product_id_idx on orders o
(cost=0.43..76.13 rows=1026 width=12)
(actual time=0.021..0.320 rows=31 loops=389)
Index Cond: (product_id = p.id)
L’estimation pour le nombre de lignes retournées par la table externe est tout à fait correct !
Mais alors c’est quoi le problème ?? 🤔
SELECT (current_setting('seq_page_cost')::numeric ×
pg_relation_size('products') / 8192 +
(
+ current_setting('cpu_tuple_cost')::numeric
+ current_setting('cpu_operator_cost')::numeric
) × 400000 / 2.4
) AS parallel_seq_scan_cost;
parallel_seq_scan_cost
------------------------
5822.3333333333333333
Sans la division par , le coût serait (au lieu de ).
Mais cela reste très supérieur au coût d’un parcours d’index !!
(coût du parcours d’index : )
Nested_loop_cost = Outer_table_scan_cost +
(index_scan_cost + cpu_tuple_cost × Ninner) × Nouter
Nested_loop_cost = Outer_table_scan_cost +
(index_scan_cost + cpu_tuple_cost × Ninner) × Nouter
= 5822 + (76.14 + 0.01 × 1026) × 162
Nested_loop_cost = Outer_table_scan_cost +
(index_scan_cost + cpu_tuple_cost × Ninner) × Nouter
= 5822 + (76.14 + 0.01 × 1026) × 162 = 19819
< 452 + (76.14 + 0.01 × 1026) × 389 = 34061
OK… mais pourquoi pas un parallel index scan ?? 🤔
min_parallel_index_scan_size
Spécifie la quantité minimale de donnée d’index qui doit être parcourue pour qu’un parcours parallèle soit envisagé. Notez qu’un parcours d’index parallèle ne touchera en général pas la totalité de l’index ;
il s’agit du nombre de pages [feuilles] que l’optimisateur pensera réellement toucher durant le parcours qui compte.
Quelle solution ?
Désactiver la parallélisation pour la table externe ?
Corriger les statistiques ?
Nested_loop_cost = Outer_table_scan_cost +
(index_scan_cost + cpu_tuple_cost × Ninner) × Nouter
= 5822 + (76.14 + 0.01 × 1026) × 162 = 19819
< 452 + (76.14 + 0.01 × 1026) × 389 = 34061
-> Nested Loop (cost=0.43..19818.47 rows=2431 width=37)
(actual time=0.071..55.054 rows=3969 loops=3)
-> Parallel Seq Scan on products p
(cost=0.00..5822.34 rows=162 width=41)
(actual time=0.025..12.601 rows=130 loops=3)
Filter: (price = 769)
Rows Removed by Filter: 133204
-> Index Scan using orders_product_id_idx on orders o
(cost=0.43..76.14 rows=1026 width=12)
(actual time=0.021..0.321 rows=31 loops=389)
Index Cond: (product_id = p.id)
Nested_loop_cost = Outer_table_scan_cost +
(index_scan_cost + cpu_tuple_cost × Ninner) × Nouter
= 5822 + (76.14 + 0.01 × 1026) × 162 = 19819
< 452 + (76.14 + 0.01 × 1026) × 389 = 34061
> 452 + (9.87 + 0.01 × 15) × 389 = 4349
Des questions ?
Dalibo recrute !! (celles et ceux qui ont tout suivi 😜)